package my.bc.webdav.client;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import my.bc.webdav.model.DavFile;
import my.bc.webdav.model.DavFold;
import my.bc.webdav.model.DavNode;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.SimpleHttpConnectionManager;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.util.URIUtil;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.webdav.DavConstants;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.MultiStatus;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.Status;
import org.apache.jackrabbit.webdav.client.methods.CopyMethod;
import org.apache.jackrabbit.webdav.client.methods.DavMethod;
import org.apache.jackrabbit.webdav.client.methods.DeleteMethod;
import org.apache.jackrabbit.webdav.client.methods.MkColMethod;
import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
import org.apache.jackrabbit.webdav.client.methods.PutMethod;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DavSession
{
  private static final Logger log = LoggerFactory.getLogger( DavSession.class );
  
  private DavSessionState sessionState;
  private ServerInfo serverInfo;
  private HttpClient client;
  private DavResultHandler davResultHandler = new DavResultTypicalHandler();
  private int maxHostConnections = 1;

  
  public DavSession( ServerInfo serverInfo )
  {
    this.serverInfo = serverInfo;
  }

  protected void ensureAuthenticated()
  {
    if( DavSessionState.AUTHENTICATED == sessionState )
      return;
    
    HostConfiguration hostConfig = new HostConfiguration();
    hostConfig.setHost( getDavHost() ); 
    
    //NOTE: use MultiThreadedHttpConnectionManager can made client hold and without response.
    HttpConnectionManager connectionManager = new SimpleHttpConnectionManager(); //new MultiThreadedHttpConnectionManager();
    HttpConnectionManagerParams params = new HttpConnectionManagerParams();
    params.setMaxConnectionsPerHost(hostConfig, maxHostConnections);
    params.setConnectionTimeout( 30000 );
    connectionManager.setParams(params);    
    client = new HttpClient(connectionManager);
    Credentials creds = new UsernamePasswordCredentials(  getDavUserName(), getDavPassword() );
    client.getState().setCredentials( AuthScope.ANY, creds );
    client.setHostConfiguration(hostConfig);

    //for httpclient version 4.3 and later
//    RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30 * 1000).build();
//    HttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();

    sessionState = DavSessionState.AUTHENTICATED;
  }

  @SuppressWarnings( "rawtypes")
  public Map< String, Map< DavPropertyName, DavProperty > > getProperties( PropFindMethod method )
      throws URISyntaxException, IOException, DavException
  {
    executeMethod( method );

    MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
    MultiStatusResponse[] responses = multiStatus.getResponses();
    
    return getProperties( responses );

  }
  
  /**
   * 
   * @param responses
   * @return Map: Item ==> ( propertyName ==> property )
   * @throws URISyntaxException
   * @throws IOException
   * @throws DavException
   */
  @SuppressWarnings( "rawtypes")
  protected Map< String, Map< DavPropertyName, DavProperty > > getProperties( MultiStatusResponse[] responses )
      throws URISyntaxException, IOException, DavException
  {
    Map< String, Map< DavPropertyName, DavProperty > > itemPropertyMap = new HashMap< String, Map< DavPropertyName, DavProperty > >();
    for ( int i = 0; i < responses.length; i++ )
    {
      MultiStatusResponse response = responses[i];
      Status[] statuses = response.getStatus();
      for( Status status : statuses )
      {
        if( status.getStatusCode() != HttpStatus.SC_OK )
          continue;   //not success
        
        String href = response.getHref();
        Map< DavPropertyName, DavProperty > propertyMap = itemPropertyMap.get( href );
        if( propertyMap == null )
        {
          propertyMap = new HashMap< DavPropertyName, DavProperty >();
          itemPropertyMap.put( href, propertyMap );
        }
        
        DavPropertySet propertySet = response.getProperties( status.getStatusCode() );
        DavPropertyName[] propertyNames = propertySet.getPropertyNames();
        for( DavPropertyName propertyName : propertyNames )
        {
          propertyMap.put( propertyName, propertySet.get( propertyName ) );
        }
      }
    }
    
    return itemPropertyMap;
  }
  
  @SuppressWarnings( "rawtypes")
  public Map< String, Map< DavPropertyName, DavProperty > > getAllProperties( String relativePath )
    throws URISyntaxException, IOException, DavException
  {
    PropFindMethod method = new PropFindMethod( getDavUriString( relativePath ), DavPropertyName.PROPFIND_ALL_PROP, DavConstants.DEPTH_0 );
    return getProperties( method );
  }
  
  @SuppressWarnings( "rawtypes")
  public Map< String, Map< DavPropertyName, DavProperty > > getProperties( String relativePath, DavPropertyNameSet propertyNameSet ) 
      throws URISyntaxException, IOException, DavException
  {
    // seems Oracle WebCenter content server doesn't support DavConstants.DEPTH_INFINITY
    PropFindMethod method = new PropFindMethod( getDavUriString( relativePath ), propertyNameSet, DavConstants.DEPTH_0 );
    return getProperties( method );
  }
  
  /**
   * curl command for dowloading file
   * curl --digest --user weblogic:fortinet123# http://fgt-oracle-feng1:16200/_dav/cs/idcplg/webdav/Enterprise%20Libraries/it-test1/test1.txt --output test1.downloaded.txt
   * @param relativePath
   * @param destFilePath
   * @throws URISyntaxException
   * @throws HttpException
   * @throws IOException
   */
  public boolean downloadFile( String relativePath, String destFilePath ) throws URISyntaxException, HttpException, IOException
  {
    ensureAuthenticated();
    
    GetMethod method = new GetMethod( getDavUriString( relativePath ) );
    int statusCode = client.executeMethod( method );
    if ( statusCode != HttpStatus.SC_OK )
    {
      System.err.println( "Method failed: " + method.getStatusLine() );
      return false;
    }
    
    InputStream in = new BufferedInputStream(method.getResponseBodyAsStream());
    File outFile = new File( destFilePath );
    OutputStream out = new BufferedOutputStream(new FileOutputStream( outFile ));
    IOUtils.copy(in,out);
    
    in.close();
    out.close();
    
    return true;
    
  }
  
  public void putFile( String relativePath, File file ) throws URISyntaxException, HttpException, IOException
  {
    ensureAuthenticated();
    
    InputStream is = new FileInputStream( file );
    RequestEntity requestEntity = new InputStreamRequestEntity( is );

    //HttpClient client = new HttpClient();
    PutMethod method = new PutMethod( getDavUriString( relativePath ) );
    method.setRequestEntity( requestEntity );

    executeMethod( method );
    
    is.close();
  }

  public void makeDirectory( String relativePath ) throws URISyntaxException, HttpException, IOException
  {
    MkColMethod method = new MkColMethod( getDavUriString( relativePath ) );
    executeMethod( method );
  }
  
  /**
   * get the resource represented by the path.
   * @param relativePath
   * @return
   * @throws URISyntaxException
   * @throws IOException
   * @throws DavException
   */
  public DavNode getSkeleton( String relativePath, boolean recursively ) throws URISyntaxException, IOException, DavException
  {
    List<DavNode> nodes = getSkeletons( relativePath, recursively, false );
    return ( ( nodes == null || nodes.isEmpty() ) ? null : nodes.get( 0 ) );
  }
  
  public List<DavNode> getSubSkeletons( String relativePath, boolean recursively ) throws URISyntaxException, IOException, DavException
  {
    return getSkeletons( relativePath, recursively, true );
  }
  
  /**
   * list the Dav Resources as DavNode
   * 
   * @param relativePath
   * @param recursively set to true if want to retrieve resource recursively
   * @param startsWithSubNode set true if retrieve sub-nodes, else retrieve the node of the path.
   * @return
   * @throws URISyntaxException
   * @throws IOException
   * @throws DavException
   */
  @SuppressWarnings( "rawtypes")
  public List<DavNode> getSkeletons( String relativePath, boolean recursively, boolean startsWithSubNode ) 
      throws URISyntaxException, IOException, DavException
  {
    DavPropertyNameSet propertyNames = new DavPropertyNameSet();
    propertyNames.add( DavPropertyName.DISPLAYNAME );
    propertyNames.add( DavPropertyName.GETCONTENTTYPE );
    propertyNames.add( DavPropertyName.ISCOLLECTION );
    propertyNames.add( DavPropertyName.RESOURCETYPE );
//    propertyNames.add( DavPropertyName.GETCONTENTLENGTH );
     
    // seems Oracle WebCenter content server doesn't support DavConstants.DEPTH_INFINITY
    PropFindMethod method = new PropFindMethod( getDavUriString(relativePath), propertyNames, 
                                                startsWithSubNode ? DavConstants.DEPTH_1 : DavConstants.DEPTH_0 );
    if( !executeMethod( method ) )
    {
      // execute method failed. should not go on
      return null;
    }

    MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
    MultiStatusResponse[] responses = multiStatus.getResponses();
    
    Map< String, Map< DavPropertyName, DavProperty > > itemPropertyMap = getProperties( responses );
    List<DavNode> nodes = new ArrayList<DavNode>();
    
    for( Entry< String, Map< DavPropertyName, DavProperty > > itemProperty : itemPropertyMap.entrySet() )
    {
      boolean isCurrentResource = isCurrentResource( getAbsolutePath( relativePath ), itemProperty.getKey() );
      //ignore the the current file or directory if retrieve from sub-nodes
      if ( startsWithSubNode && isCurrentResource )
      {
        continue;
      }
      
      nodes.add( buildDavNode( relativePath, itemProperty.getValue(), isCurrentResource, recursively ) );
    }

    return nodes;
  }
  
  protected boolean isCurrentResource( String absoluteRawPath, String itemHref )
  {
    try
    {
      if( absoluteRawPath.endsWith( "/" ) )
        absoluteRawPath = absoluteRawPath.substring( 0, absoluteRawPath.length()-1 );
      
      if( itemHref.endsWith( "/" ) )
        itemHref = itemHref.substring( 0, itemHref.length()-1 );
      return itemHref.equals( absoluteRawPath ) || UriUtil.deepEscape( itemHref ).equals( UriUtil.deepEscape( absoluteRawPath ) );
    }
    catch( Exception e )
    {
      log.warn( "", e );
      return false;
    }
  }
  
  
  /**
   * 
   * @param relativePath The path to the resource
   * @param propertySet
   * @param recursively
   * @return
   * @throws URISyntaxException
   * @throws IOException
   * @throws DavException
   */
  @SuppressWarnings( "rawtypes")
  protected DavNode buildDavNode( String relativePath, Map< DavPropertyName, DavProperty > propertyMap, boolean isCurrentResource, boolean recursively ) 
      throws URISyntaxException, IOException, DavException
  {
    DavProperty davProperty = propertyMap.get( DavPropertyName.DISPLAYNAME );
    String name = davProperty == null ? null : (String)davProperty.getValue();
    String itemPath = ( isCurrentResource ? relativePath : AbstractServerInfo.appendRawPath( relativePath, name ) );
    
    if( isFold( propertyMap, itemPath ) )
    {
      DavFold fold = new DavFold( name );
      if( recursively )
      {
        fold.setChildren( getSkeletons( itemPath, recursively, true ) );
      }
      return fold;
    }
    else
      return new DavFile( name );
  }
  
  @SuppressWarnings( "rawtypes")
  public boolean isFold( Map< DavPropertyName, DavProperty > propertyMap, String relativePath ) throws URISyntaxException, IOException
  {
    Boolean isFold = null;
    if( propertyMap != null && !propertyMap.isEmpty() )
    {
      try
      {
        isFold = isFold( propertyMap );
      }
      catch( UnknownResource ur )
      {
      }
    }
    if( isFold == null )
    {
      if( relativePath != null && !relativePath.isEmpty() )
      {
        try
        {
          isFold = isFold( relativePath );
        }
        catch( UnknownResource ur )
        {
        }
      }
    }
    
    //treat as non-fold is isFold is null;
    return ( isFold != null && isFold );

  }
  
  @SuppressWarnings( "rawtypes")
  protected boolean isFold( Map< DavPropertyName, DavProperty > propertyMap ) throws UnknownResource
  {
    //isCollection
    DavProperty davProperty;
    {
      davProperty = propertyMap.get( DavPropertyName.ISCOLLECTION );
      Boolean isCollection = ( davProperty == null ? null : (Boolean)davProperty.getValue() );
      if( isCollection != null )
        return isCollection;
    }

    //content-type
    {
      davProperty = propertyMap.get( DavPropertyName.GETCONTENTTYPE );
      String contentType = ( davProperty == null ? null : (String)davProperty.getValue() );
      if( contentType != null && !contentType.isEmpty() && contentType.toLowerCase().contains( "directory" ) )
        return true;
    }
    
    //resource-type
    {
      davProperty = propertyMap.get( DavPropertyName.RESOURCETYPE );
      Object resourceType = ( davProperty == null ? null : davProperty.getValue() );
      if( resourceType != null && resourceType.toString().toLowerCase().contains( "collection" ) )
        return true;
    }
    
    throw new UnknownResource();
  }
  
  /**
   * The typical way to judge an item is fold/collection or not is by checking it's properties.
   * But seems WedDav doesn't definition on it. And different WebDav server are vary.
   * So, try to implement it by trying to fetching properties of sub-items
   * @param relativePath
   * @return
   * @throws IOException 
   * @throws URISyntaxException 
   */
  public boolean isFold( String relativePath ) throws URISyntaxException, IOException, UnknownResource
  {
    try
    {
      DavPropertyNameSet propertyNames = new DavPropertyNameSet();
      propertyNames.add( DavPropertyName.DISPLAYNAME );
      propertyNames.add( DavPropertyName.GETCONTENTTYPE );
      propertyNames.add( DavPropertyName.ISCOLLECTION );
      propertyNames.add( DavPropertyName.RESOURCETYPE );
//      propertyNames.add( DavPropertyName.GETCONTENTLENGTH );
       
      // seems Oracle WebCenter content server doesn't support DavConstants.DEPTH_INFINITY
      PropFindMethod method = new PropFindMethod( getDavUriString( relativePath ), propertyNames, DavConstants.DEPTH_1 );
      executeMethod( method );

      MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
      MultiStatusResponse[] responses = multiStatus.getResponses();
      
      int subResourceCount = 0;
      for ( int i = 0; i < responses.length; i++ )
      {
        if( subResourceCount >= 1 )
          return true;
        
        MultiStatusResponse response = responses[i];
        boolean isCurrentResource = isCurrentResource( getAbsolutePath( relativePath ), response.getHref() );

        if (  isCurrentResource )
        {
          continue;
        }
        
        Status[] statuses = response.getStatus();
        for( Status status : statuses )
        {
          if( status.getStatusCode() != HttpStatus.SC_OK )
            continue;   //not success
          ++subResourceCount;
        }
      }
      
      if( subResourceCount >= 1 )
        return true;
      
      //the sub resource is zero, it could be a empty fold or a file
      throw new UnknownResource( "The resource could be a file or empty fold." );
      
    }
    catch( DavException davE )
    {
      return false;
    }
  }
  
  public void deleteResource( String relativePath ) throws URISyntaxException, HttpException, IOException
  {
    
    DeleteMethod method = new DeleteMethod( getDavUriString( relativePath ) );
    executeMethod( method );
  }
  
  
  public void copyResource( String sourcePath, String destinationPath ) throws URISyntaxException, HttpException, IOException, DavException
  {
    CopyMethod method = new CopyMethod( getDavUriString( sourcePath ), 
                                        getDavUriString( destinationPath ),
                                        true, true );
    
    executeMethod( method );
  }
  
  protected boolean executeMethod( DavMethod method ) throws HttpException, IOException
  {
    ensureAuthenticated();
    
    client.executeMethod( method );
    
    if( !method.succeeded() )
    {
      onFailed( method );
      return false;
    }
    else
    {
      onSucceeded( method );
      return true;
    }
  }
  
  protected void onFailed( DavMethod method )
  {
    if( davResultHandler != null )
      davResultHandler.onFailed( method );
  }

  protected void onSucceeded( DavMethod method )
  {
    if( davResultHandler != null )
      davResultHandler.onSucceeded( method );
  }


  protected String getDavHost()
  {
    return serverInfo==null ? null : serverInfo.getHost();
  }
  
  protected String getDavInitialPath()
  {
    return serverInfo==null ? null : serverInfo.getInitialPath();
  }

  protected String getDavUserName()
  {
    return serverInfo==null ? null : serverInfo.getUserName();
  }

  protected String getDavPassword()
  {
    return serverInfo==null ? null : serverInfo.getPassword();
  }
  
  protected String getDavUriString( String path) throws URISyntaxException
  {
    return serverInfo==null ? null : serverInfo.getUriString( path );
  }
  
  protected String getAbsolutePath( String relativePath )
  {
    return serverInfo==null ? null : ( serverInfo.getInitialPath() + relativePath );
  }
  
}
